package com.dongjiantong.openplatform.core;

import com.dongjiantong.openplatform.annotations.OpenBean;
import com.dongjiantong.openplatform.annotations.OpenMethod;
import com.dongjiantong.openplatform.exceptions.DuplicatedOpenMethodKey;
import com.dongjiantong.openplatform.exceptions.OPClientException;
import com.dongjiantong.openplatform.exceptions.OPServerException;
import com.dongjiantong.openplatform.utils.OPParameterResolver;
import com.dongjiantong.openplatform.validate.InvokeValidator;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @author cao
 * @create 2017-12-13 下午7:42
 **/
@Component
public class OPInvoker {
    private static final Logger log = LoggerFactory.getLogger(OPInvoker.class);
    private final ConcurrentMap<String, MethodInfo> keyToMethods;
    private final ApplicationContext applicationContext;

    @Autowired
    public OPInvoker(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        this.keyToMethods = new ConcurrentHashMap();
    }

    @PostConstruct
    public void init() {
        Map<String, Object> openBeans = this.applicationContext.getBeansWithAnnotation(OpenBean.class);
        Iterator var2 = openBeans.values().iterator();

        while(var2.hasNext()) {
            Object openBean = var2.next();
            this.handleOpenBean(openBean);
        }

    }

    public OPInvoker.MethodInfo getMethodInfo(String method) {
        return (OPInvoker.MethodInfo)this.keyToMethods.get(method);
    }

    private void handleOpenBean(Object openBean) {
        for(Class klass = openBean.getClass(); klass != null; klass = klass.getSuperclass()) {
            List<Method> allMethods = Arrays.asList(klass.getDeclaredMethods());
            Iterator var4 = allMethods.iterator();

            while(var4.hasNext()) {
                Method method = (Method)var4.next();
                if(method.isAnnotationPresent(OpenMethod.class)) {
                    OpenMethod openMethodAnn = (OpenMethod)method.getAnnotation(OpenMethod.class);
                    String methodKey = openMethodAnn.key();
                    RequestMethod[] httpMethods = openMethodAnn.httpMethods();
                    Class<?>[] paramTypes = method.getParameterTypes();
                    String[] paramNames = openMethodAnn.paramNames();
                    Class<? extends PushPostHandle> pushPostHandle = openMethodAnn.pushPostHandleClass();
                    OPInvoker.MethodInfo methodInfo = new OPInvoker.MethodInfo(openBean, method, httpMethods, paramTypes, paramNames, pushPostHandle);
                    if(this.keyToMethods.putIfAbsent(methodKey, methodInfo) != null) {
                        log.error("duplicate open method key:({})", methodKey);
                        throw new DuplicatedOpenMethodKey("duplicated open method key: " + methodKey);
                    }
                }
            }
        }

    }

    public Object execute(String methodKey, String httpMethod, Map<String, Object> params, boolean isRequestBody) {
        OPInvoker.MethodInfo methodInfo = (OPInvoker.MethodInfo)this.keyToMethods.get(methodKey);
        if(methodInfo == null) {
            throw new OPClientException(404, "method.target.not.found");
        } else {
            if(methodInfo.httpMethods.length > 0) {
                boolean acceptHttpMethod = false;
                RequestMethod[] var7 = methodInfo.httpMethods;
                int var8 = var7.length;

                for(int var9 = 0; var9 < var8; ++var9) {
                    RequestMethod method = var7[var9];
                    if(Objects.equals(method.name(), httpMethod)) {
                        acceptHttpMethod = true;
                        break;
                    }
                }

                if(!acceptHttpMethod) {
                    throw new OPClientException(405, "method.not.allowed");
                }
            }

            try {
                return methodInfo.invoke(params, isRequestBody);
            } catch (Exception var11) {
                log.error("failed to invoke {}, real params is {}, cause:{}", new Object[]{methodInfo, params, Throwables.getStackTraceAsString(var11)});
                Throwables.propagateIfInstanceOf(var11, OPClientException.class);
                Throwables.propagateIfInstanceOf(var11, OPServerException.class);
                 //throw new OPServerException();
                throw Throwables.propagate(var11);

            }
        }
    }

    public static class MethodInfo {
        private final Object ownerBean;
        private final Method method;
        private final RequestMethod[] httpMethods;
        private final Class<?>[] paramTypes;
        private final String[] paramNames;
        private final Class<? extends PushPostHandle> pushPostHandleClass;

        public Class<? extends PushPostHandle> getPushPostHandleClass() {
            return this.pushPostHandleClass;
        }

        public MethodInfo(Object ownerBean, Method method, RequestMethod[] httpMethods, Class<?>[] paramTypes, String[] paramNames, Class<? extends PushPostHandle> pushPostHandleClass) {
            this.ownerBean = ownerBean;
            this.method = method;
            this.httpMethods = httpMethods;
            this.paramTypes = paramTypes;
            this.paramNames = paramNames;
            this.pushPostHandleClass = pushPostHandleClass;
        }

        public Object invoke(Map<String, Object> params, boolean isRequestBody) {
            Object[] concernedParams = new Object[this.paramTypes.length];
            if(isRequestBody && this.paramTypes.length > 1) {
                throw new OPClientException(400, "method.param.not.allowed");
            } else {
                for(int i = 0; i < this.paramTypes.length; ++i) {
                    Class<?> type = this.paramTypes[i];
                    String paramName = this.paramNames[i];

                    try {
                        if(isRequestBody) {
                            Object requestBody = params.get("data");
                            concernedParams[i] = OPParameterResolver.resolve(type, paramName, ImmutableMap.of(paramName, requestBody));
                        } else {
                            concernedParams[i] = OPParameterResolver.resolve(type, paramName, params);
                        }
                    } catch (Exception var8) {
                        OPInvoker.log.error("{} has no default constructor, cause:{}", type, Throwables.getStackTraceAsString(var8));
                        throw new OPServerException(500, "server.internal.error");
                    }
                }

                InvokeValidator.instance().validateParams(this.ownerBean, this.method, concernedParams);
                return ReflectionUtils.invokeMethod(this.method, this.ownerBean, concernedParams);
            }
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this).add("ownerBean", this.ownerBean).add("method", this.method).add("paramTypes", this.paramTypes).add("paramNames", this.paramNames).omitNullValues().toString();
        }
    }
}
